﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using NMapper.Utils;

namespace NMapper.Mappings
{
    public class SequenceMapping<TDatabase, TParentDbEntity, TParentDbValue, TChildDbEntity, TParentOutput, TChildOutput, TOutputValue> : IDbValueMapping<TDatabase, TParentDbEntity, TParentDbValue>
        where TChildDbEntity : class
        where TOutputValue : class
        where TChildOutput : class
        where TDatabase : IDisposable
    {
        private Expression<Func<MapContext<TDatabase>, TParentDbEntity, TParentDbValue>> dbSelector;
        private Func<MapContext<TDatabase>, TParentDbEntity, TParentDbValue> dbSelectorFunc;
        private Expression<Func<TParentOutput, TOutputValue>> outputSelector;
        private Mapper<TDatabase, TChildDbEntity, TChildOutput> mapper;
        private PropertyInfo property;

        public SequenceMapping(Expression<Func<MapContext<TDatabase>, TParentDbEntity, TParentDbValue>> dbSelector, Expression<Func<TParentOutput, TOutputValue>> outputSelector, Mapper<TDatabase, TChildDbEntity, TChildOutput> mapper)
        {
            this.dbSelector = dbSelector;
            dbSelectorFunc = dbSelector.Compile();
            this.outputSelector = outputSelector;
            this.mapper = mapper;
        }

        LambdaExpression IDbSelector<TDatabase>.DbSelectorExpression
        {
            get { return dbSelector; }
        }

        Func<MapContext<TDatabase>, object, object> IDbSelector<TDatabase>.DbSelector
        {
            get { return (mapContext, x) => DbSelector(mapContext, (TParentDbEntity)x); }
        }

        public Func<MapContext<TDatabase>, TParentDbEntity, TParentDbValue> DbSelector
        {
            get { return dbSelectorFunc; }
        }

        public PropertyInfo TransientProperty
        {
            get { return property; }
        }

        public void DefineTransientType(ModuleBuilder module, TypeBuilder type)
        {
            mapper.Initialize();
            var propertyName = NameHelpers.SuggestName(dbSelector, outputSelector);
            property = type.DefineProperty(propertyName, mapper.TransientType.MakeArrayType());
        }

        public void DefineTransientInitialization(MapContext<TDatabase> mapContext, Type transientType, List<MemberBinding> initializers, ParameterExpression parameterExpression)
        {
            property = transientType.GetProperty(property.Name);

            var lambdaBinder = new LambdaBinder();
            var simpleDbSelector = dbSelector.PartialApplication(Expression.Constant(mapContext), null).Simplify();
            var boundDbSelector = lambdaBinder.Bind(simpleDbSelector, parameterExpression);

            var entitySet = boundDbSelector.Body;
            var subSelector = mapper.CreateTransientSelector(mapContext);

            var transientSelectMethod = typeof(Enumerable).GetGenericMethod("Select", new[] { typeof(TChildDbEntity), mapper.TransientType }, new[] { typeof(IEnumerable<TChildDbEntity>), subSelector.Type }); 
            var transientToArrayMethod = typeof(Enumerable).GetGenericMethod("ToArray", new[] { mapper.TransientType }, new[] { typeof(IEnumerable<>).MakeGenericType(mapper.TransientType) }); 

            var callSelect = Expression.Call(transientSelectMethod, entitySet, subSelector);
            var callToArray = Expression.Call(transientToArrayMethod, callSelect);
            var initializer = Expression.Bind(property.GetSetMethod(), callToArray);
            initializers.Add(initializer);
        }

        void IMapping<TDatabase>.OnTransientInitialized(MapContext<TDatabase> mapContext, Array transient)
        {
            foreach (var item in transient)
            {
                var subValue = (Array)property.GetValue(item, null);
                mapper.OnTransientInitialized(mapContext, subValue);                
            }
        }

        public void MapToOutput(MapContext<TDatabase> mapContext, object transient, object output)
        {
            var outputProperty = outputSelector.GetPropertyInfo();
            var subValue = (Array)property.GetValue(transient, null);

            var outputValue = mapper.TransformToOutput(mapContext, subValue);
            object value = outputValue;
            if (outputProperty.PropertyType.IsGenericList())
                value = outputValue.ToList();

            outputProperty.SetValue(output, value, null);
        }

        public void MapToInput(MapContext<TDatabase> mapContext, object output, object input)
        {
            var dbItems = (IEnumerable<TChildDbEntity>)DbSelector(mapContext, (TParentDbEntity)input);
            var dbList = dbItems as IList<TChildDbEntity>;

            var outputProperty = outputSelector.GetPropertyInfo();
            var outputItems = (IList<TChildOutput>)outputProperty.GetValue(output, null);

            if (dbList != null)
            {
                var dbItemsById = dbList.ToDictionary(x => mapper.PrimaryKey.DbSelector(mapContext, x));
                var existingDbItemIds = dbList.Select(x => mapper.PrimaryKey.DbSelector(mapContext, x));
                var existingOutputIds = outputItems.Select(x => mapper.PrimaryKey.OutputSelector(x));

                var merge = existingDbItemIds.Merge(existingOutputIds);

                foreach (var id in merge.Removed)
                    dbList.Remove(dbItemsById[id]);
            }

            IEnumerable<TChildDbEntity> inserted;
            mapper.TransformToInput(mapContext, outputItems.ToArray(), ids => dbItems, out inserted);

            if (dbList != null)
            {
                foreach (var item in inserted) 
                    dbList.Add(item);
            }
        }

        public override string ToString()
        {
            return string.Format("SequenceMapping({0}, {1}, {2})", dbSelector, outputSelector, mapper);
        }
    }
}